# Music Library - Interface for Entertainer music library cache
# Copyright (C) 2007 Lauri Taimila
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

__licence__ = "GPLv2"
__copyright__ = "2007, Lauri Taimila"
__author__ = "Lauri Taimila <lauri@taimila.com>"

import os
import CDDB, DiscID
from pysqlite2 import dbapi2 as sqlite
from frontend.medialibrary.playable import Playable
from utils.lyrics_downloader import LyricsDownloader

################################################################################
# This file contains three classes that together consists music library 
# interface. These classes are MusicLibrary, Album and Track.
################################################################################

class MusicLibraryException(Exception): pass
class AlbumHasNoTracks(Exception): pass
class TrackTypeError(TypeError): pass
class TrackRatingOutOfRange(Exception): pass

class MusicLibrary:
    """
    Music library. 
    
    Interface for Entertainer's music cache.
    """
    
    def __init__(self, backend_connection, music_db=os.path.expanduser('~/.entertainer/cache/music.db')):
        """
        Initialize library.
        @param backend_connection: BackendConnection object
        """
        self.music_db = music_db
        if not os.path.exists(self.music_db):
            raise Exception("Music database doesn't exist!")
        self.backend_connection = backend_connection
        self.db_connection = sqlite.connect(self.music_db)
        self.cursor = self.db_connection.cursor()
    
    def __del__(self):
        """
        Close connection when this object is destroyed
        """
        self.db_connection.close()

    def get_compact_disc_information(self):
        """
        Get the CompactDisc object of the current disc in drive. This
        method queries CD information from Internet database (CDDB).
        @return: CompactDisc
        """
        cdrom = DiscID.open()
        try:
            disc_id = DiscID.disc_id(cdrom)
        except:
            raise Exception("No media in drive.")
        return CompactDisc(disc_id)
        
    def get_playlists(self):
        """
        Get list of playlist names.
        @return: String list
        """
        self.cursor.execute("SELECT title FROM playlist")
        playlists = []
        for row in self.cursor:
            playlists.append(row[0])
        return playlists
        
    def get_number_of_tracks_on_playlist(self, name):
        """
        Get list of playlist names.
        @param name: Playlist name (see: get_playlists)
        @return: Integer (number of tracks)
        """
        self.cursor.execute("""SELECT COUNT(filename) 
                          FROM playlist_relation 
                          WHERE title=:name""", { "name" : name })
        result = self.cursor.fetchall()
        return result[0][0]
        
    def get_tracks_on_playlist(self, name):
        """
        Retrun list of tracks on given playlist.
        @param name: Playlist name
        @return: List of Track objects
        """
        self.cursor.execute("""SELECT filename, title, tracknumber,  artist,  album, 
                                 genre, bitrate, year, rating, length, comment, 
                                 lyrics 
                          FROM   track, playlist_relation
                          WHERE  playlist_relation.title=:name
                          AND    playlist_relation.filename=track.filename
                          ORDER BY title""", { "name" : name })
        tracks = []
        for row in self.cursor:
            tracks.append(Track(row[0], row[1], row[2], row[3], row[4], 
                                row[5], row[6], row[7], row[8], row[9], 
                                row[10], row[11]))
        return tracks
    
    def get_all_artists(self):
        """
        Get a list of all artists.
        @return: list of Strings 
        """
        self.cursor.execute("SELECT DISTINCT artist FROM track ORDER BY artist")
        artists = []
        for row in self.cursor:
            artists.append(row[0])
        return artists
        
    def get_all_tracks(self):
        """
        Get all tracks in music cache.
        @return: List of Track objects
        """
        self.cursor.execute("""SELECT filename, title, tracknumber,  artist,  album, 
                                 genre, bitrate, year, rating, length, comment, 
                                 lyrics 
                          FROM   track 
                          ORDER BY title""")
        tracks = []
        for row in self.cursor:
            tracks.append(Track(row[0], row[1], row[2], row[3], row[4], 
                                row[5], row[6], row[7], row[8], row[9], 
                                row[10], row[11]))
        return tracks
        
    def get_tracks_by_genre(self, genre):
        """
        Get albums from music cache by genre.
        @param genre: Genre (string)
        @return: List of Track objects
        """
        self.cursor.execute("""SELECT filename, title, tracknumber,  artist,  album, 
                                 genre, bitrate, year, rating, length, comment, 
                                 lyrics 
                          FROM   track
                          WHERE  genre=:genre
                          ORDER BY title""", { "genre" : genre})
        tracks = []
        for row in self.cursor:
            tracks.append(Track(row[0], row[1], row[2], row[3], row[4], 
                                row[5], row[6], row[7], row[8], row[9], 
                                row[10], row[11]))
        return tracks
     
    def get_tracks_by_artist(self, artist):
        """
        Get tracks from music cache by artist.
        @param artist: Name of the artist (string)
        """
        self.cursor.execute("""SELECT filename, title, tracknumber,  artist,  album, 
                                 genre, bitrate, year, rating, length, comment, 
                                 lyrics 
                          FROM   track
                          WHERE  artist=:artist
                          ORDER BY title""", { "artist" : artist})
        tracks = []
        for row in self.cursor:
            tracks.append(Track(row[0], row[1], row[2], row[3], row[4], 
                                row[5], row[6], row[7], row[8], row[9], 
                                row[10], row[11]))
        return tracks
            
    def get_all_albums(self):
        """
        Get all albums in music cache.
        @return: List of Album objects
        """
        self.cursor.execute("SELECT DISTINCT album FROM track ORDER BY album")
        albums = []
        for row in self.cursor.fetchall():
            albums.append(Album(row[0], self.cursor))
        return albums
        
    def get_albums_by_artist(self, artist):
        """
        Get albums from music cache by artist.
        @param artist: Artist name (string)
        @return: List of Album objects
        """
        self.cursor.execute("""SELECT DISTINCT album 
                          FROM track 
                          WHERE artist=:artist
                          ORDER BY album""", { "artist" : artist})
        albums = []
        for row in self.cursor.fetchall():
            albums.append(Album(row[0], self.cursor))
        return albums

    def number_of_tracks(self):
        """
        Get the number of tracks in music cache.
        @return: Integer (number of tracks)
        """
        self.cursor.execute("SELECT COUNT(filename) FROM track")
        result = self.cursor.fetchall()
        return result[0][0]
        
    def number_of_tracks_by_artist(self, artist):
        """
        Get the number of tracks made by given artist
        @param artist: Artist name (string)
        """
        self.cursor.execute("""SELECT COUNT(filename) 
                          FROM track 
                          WHERE artist=:artist""", { "artist" : artist})
        result = self.cursor.fetchall()
        return result[0][0]

    def number_of_albums_by_artist(self, artist):
        """
        Get the number of albums made by given artist
        @param artist: Artist name (string)
        """
        self.cursor.execute("""SELECT DISTINCT album 
                          FROM track 
                          WHERE artist=:artist
                          ORDER BY album""", { "artist" : artist})
        result = self.cursor.fetchall()
        return len(result)

    def save_lyrics(self, track, lyrics):
        """
        Save lyrics to database.
        @param track: Track which lyrics we are saving
        @param lyrics: Lyrics string 
        """
        track.set_lyrics(lyrics) # Be certain to save for this track instance,
                                 # not just persistent storage
        self.cursor.execute("""UPDATE track
                               SET lyrics=:lyrics
                               WHERE filename=:fn""", 
                               { "lyrics" : lyrics, "fn" : track.get_filename() })
        self.db_connection.commit()

################################################################################

class Album:
    """
    Objects from this class represents albums. Album contains tracks.
    """
        
    def __init__(self, title, cursor=False):
        """Initialize album (Fetch data from music cache)"""
        self.tracks = []
        self.title = title
        self.total_length = 0
        #FIXME: Album needs to be refactored so that it has no dependence on the database.
        if not cursor:
            music_db=os.path.expanduser('~/.entertainer/cache/music.db')
            self.db_connection = sqlite.connect(music_db)
            self.cursor = self.db_connection.cursor()
        else:
            self.cursor = cursor
        self.cursor.execute("""SELECT filename, title, tracknumber,  artist,  album, 
                                 genre, bitrate, year, rating, length, comment, 
                                 lyrics 
                          FROM   track 
                          WHERE  album=:title 
                          ORDER BY tracknumber""", {"title" : title})

        for row in self.cursor:
            self.tracks.append(Track(row[0], row[1], row[2], row[3], self, 
                                     row[5], row[6], row[7], row[8], row[9], 
                                     row[10], row[11]))
            #self.artist = row[3] #FIXME: Should we set artist somewhere else?
            self.total_length += int(row[9])
        if len(self.tracks) == 0:
            raise AlbumHasNoTracks()
        self.artist = self.tracks[0].artist

    def __str__(self):
        return self.title
        #return '%s - %s' % (self.artist, self.title)
    
    def get_title(self):
        """
        Get title ot the album.
        @return: String
        """
        return self.title
        
    def has_album_art(self):
        """
        Has this album album art graphics.
        @return: Boolean
        """
        # TODO: MediaLibrary should probably be the object that knows the path
        # to the ablum art, not the album itself
        path = os.path.expanduser('~/.entertainer/cache/album_art')
        album_art = os.path.join(path, self.artist + " - " + self.title + ".jpg")
        return os.path.exists(album_art)
    
    def get_album_art_url(self):
        """
        Get absolute path of album art file or raises Exception.
        @return: String
        """
        # TODO: MediaLibrary should probably be the object that knows the path
        # to the ablum art, not the album itself
        path = os.path.expanduser('~/.entertainer/cache/album_art') # Create optimized 150x150 images to ./small
        return os.path.join(path, self.artist + " - " + self.title + ".jpg")
    
    def get_tracks(self):
        """
        Get tracks from this Album.
        @return: List of Track object
        """
        return self.tracks
        
    def get_number_of_tracks(self):
        """
        Get the number of tracks on this album.
        @return: Integer
        """
        return len(self.tracks)
        
    def get_year(self):
        """
        Get release year of this album. This is taken from the first track.
        @return: String
        """
        return self.tracks[0].get_year()
        
    def get_genre(self):
        """
        Get genre of this album. This is taken from the first track.
        @return: String
        """
        return self.tracks[0].get_genre()
        
    def get_artist(self):
        """
        Get the artist of this album. This is taken from the first track.
        @return: String 
        """
        return self.tracks[0].get_artist()
    
    def get_total_length(self):
        """
        Get total length of the album.
        @return: Integer (Length in seconds)
        """
        return self.total_length
        
################################################################################

class Track(Playable):
    """
    Track is a Playable object that represents one song in Music library.
    """
    
    def __init__(self, filename, title, tracknumber, artist, album, genre, 
                 bitrate, year, rating, length, comment, lyrics):
        """
        Initialize track. Notice that album parameter can be either
        Album name as string or Album object! getAlbum() returns always object.
        """
        
        # Check that these four fields are integers
        for input in [tracknumber, year, rating, length]:
            if rating == None: # By default, there is no rating for a track
                continue
            if type(input) != int:
                raise TrackTypeError("%s is not an integer" % input)
            
        # Check that the rating is in range 1-5
        if rating != None and rating not in range(1,6):
            raise TrackRatingOutOfRange()
            
        self.filename = str(filename)     # Filename of the track
        self.title = str(title)           # Track title
        self.tracknumber = tracknumber    # Number of the track
        self.artist = str(artist)         # Artist of the track
        if isinstance(album, Album):
            self.album = album            # Album that contains this track (str OR Album object!)
        else:
            self.album = str(album)
        self.genre = str(genre)           # Genre of the track
        self.bitrate = str(bitrate)       # Bitrate of the track
        self.year = year                  # Release year
        self.rating = rating              # Rating of the track (1-5)
        self.length = length              # Length of the track in seconds (example 240)
        self.comment = str(comment)       # Comment about this track
        self.lyrics = str(lyrics)         # Lyrics of the track

    def get_filename(self):
        """
        Get filename of the track
        @return: String
        """
        return self.filename
        
    def get_title(self):
        """
        Get title ot the track__MUSIC_DB
        @return: String
        """
        return self.title
        
    def get_tracknumber(self):
        """
        Get track number of the track
        @return: Interger
        """
        return self.tracknumber
        
    def get_artist(self):
        """
        Get artist of the track
        @return: String
        """
        return self.artist
        
    def get_album(self, cursor=False):
        """
        Get album that contains this Track.
        @return: Album object
        """
        #FIXME: when Album constructor removes work-around, get_album needs to
        # remove the default False and get the cursor from the media library
        if not isinstance(self.album, Album):
            album = Album(self.album, cursor)
            self.album = album
            return album
        else:
            return self.album
    
    def get_album_art_url(self, cursor=False):
        """
        Get album art URL of this Track
        @return: String (or None if there is no album art for this track)
        """
        #FIXME: when Album constructor removes work-around, get_album_art_url
        # needs to remove the default False and get the cursor from the media
        # library
        self.get_album(cursor) # Need to have an album object
            
        if self.album.has_album_art():
            return self.album.get_album_art_url()
        else:
            return None
            
    def get_genre(self):
        """
        Get genre of the track
        @return: String
        """
        return self.genre
        
    def get_bitrate(self):
        """
        Get bitrate of the track
        @return: String
        """
        return self.bitrate
        
    def get_year(self):
        """
        Get year of the track
        @return: Integer
        """
        return self.year
        
    def get_rating(self):
        """
        Return rating of the track
        @return: Integer
        """
        return self.rating

    def get_length(self):
        """
        Get length of the track in seconds
        @return: Integer
        """
        return self.length
    
    def get_length_string(self):
        """
        Get length of the track as human readable string. Example: 02:46
        @return: Timestamp as string
        """
        return str.zfill(str(self.length / 60), 2) + ":" + str.zfill(str(self.length % 60), 2)
        
    def get_comment(self):
        """
        Get comment of the track
        @return: String
        """
        return self.comment
    
    def has_lyrics(self):
        """
        Is there lyrics for this track in music cache
        @return: Boolean, true if lyrics exists
        """
        if self.lyrics == "":
            return False
        else:
            return True
        
    def fetch_lyrics(self, callback=False):
        """
        Fetch lyrics from the Internet using the LyricsDownloader, use
        callback function to indicate completion since LyricsDownloader is
        asynchronous. Callback function must take lyrics as only input
        parameter.
        @param callback: Function called when fetch_lyrics completes
        """
        if not callback:
            callback = self.set_lyrics
        if not self.has_lyrics():
            self.ld = LyricsDownloader(self.get_title(), self.get_artist(),
                                       callback)
            self.ld.search()
                
    def get_lyrics(self):
        """
        Get lyrics of the track
        @return: String
        """
        return self.lyrics
    
    def set_lyrics(self, lyrics):
        """
        Set lyrics to track object instance, use save lyrics for persistent
        storage of lyrics
        @param lyrics: Lyrics of track
        """
        if lyrics is None:
            self.lyrics = ""
        else:
            self.lyrics = lyrics

    # Implement playable interface
    def get_type(self):
        """
        Track is an audio stream.
        @return: Integer
        """
        return Playable.AUDIO_STREAM
    
    def get_uri(self):
        """
        Get URI of the audio file
        @return: String
        """
        return "file://" + self.filename

################################################################################
    
class CompactDisc(object):
    """
    CompactDisc
    
    Represents one CD. This object is a simple container that includes CD artist,
    album name and track listing.
    """
    
    def __init__(self, disc_id):
        """
        Create a new CompactDisc object. Initialization queries track information
        form the Internet.
        @param disc_id: DiscID
        """
        self.tracks = []

        (query_status, query_info) = CDDB.query(disc_id)
        
        #FIXME: query_info contains code that we could use instead of TRY EXCEPT
        #       See CDDB documentation for more information.
        #       http://cddb-py.sourceforge.net/CDDB/README
        
        if query_info is not None:
            # query_info variable's type depends on how many matches we get. If we
            # get just one match then it's a map and if we get more than one it's
            # a list.
            try:
                self.artist = query_info['title'][:query_info['title'].index(' / ')]
                self.album = query_info['title'][query_info['title'].index(' / ') + 3:]
            
                # Get track titles
                (read_status, read_info) = CDDB.read(query_info['category'], query_info['disc_id'])
                cumulative_length = 0
                for i in range(disc_id[1]):
                    if i + 4 == len(disc_id):
                        # We must calculate last track length different way
                        length = disc_id[len(disc_id) - 1] - cumulative_length
                    else:
                        length = (disc_id[i+3] - disc_id[i+2]) / 75 # Calculate track length in seconds
                        cumulative_length = cumulative_length + length

                    track_title = read_info['TTITLE' + str(i)]
                    self.tracks.append(CompactDiscTrack(i + 1, track_title, length))
            except:
                self.artist = query_info[0]['title'][:query_info[0]['title'].index(' / ')]
                self.album = query_info[0]['title'][query_info[0]['title'].index(' / ') + 3:]
            
                # Get track titles
                (read_status, read_info) = CDDB.read(query_info[0]['category'], query_info[0]['disc_id'])
                cumulative_length = 0
                for i in range(disc_id[1]):
                    if i + 4 == len(disc_id):
                        # We must calculate last track length different way
                        length = disc_id[len(disc_id) - 1] - cumulative_length
                    else:
                        length = (disc_id[i+3] - disc_id[i+2]) / 75 # Calculate track length in seconds
                        cumulative_length = cumulative_length + length
                        
                    track_title = read_info['TTITLE' + str(i)]
                    self.tracks.append(CompactDiscTrack(i + 1, track_title, length))
        else:
            # No metadata found for this disc
            self.album = "Unknown title"
            self.artist = "Unknown artist"
            for i in range(disc_id[1]):
                self.tracks.append(CompactDiscTrack(i + 1, "Unknown track " + str(i+1) + "."))
    
    def get_artist(self):
        """
        Get the name of the artist
        @return: Artist name as string
        """
        return self.artist
        
    def get_title(self):
        """
        Get name of the album.
        @return: name as string
        """
        return self.album
    
    def get_tracks(self):
        """
        Get a list of track names.
        @return: Tracknames in a string list
        """
        return self.tracks
    
    def get_length(self):
        """
        Get length of the album. This is the sum of the track lengths.
        @return: Integer, length in seconds 
        """
        length = 0
        for track in self.tracks:
            length = length + track.get_length()
        return length
    
################################################################################
    
class CompactDiscTrack(Playable):
    """
    CompactDiscTrack is a Playable object that is a one track of the Audio CD.
    """
    
    def __init__(self, track_number, title, track_length):
        """
        Create a new CD track.
        @param track_number: Number of the track on the Audio CD.
        @param title: Title of the track
        @param track_length: Track length in seconds (integer)
        """
        self.title = title
        self.uri = "cdda://" + str(track_number)
        self.track_length = track_length
    
    def get_title(self):
        """
        Get title of the track
        @return: String
        """
        return self.title
    
    def get_uri(self):
        """
        Get URI of the playable stream.
        @return: String
        """
        return self.uri
    
    def get_length(self):
        """
        Get track length in seconds
        @return: Integer, length in seconds 
        """
        return self.track_length
        
    def get_type(self):
        """
        Get type of the stream. This is one of the 
        constants defined in Playable class.
        @return: Integer
        """
        return Playable.AUDIO_STREAM
    
